/**
 * 
 */
package com.nextive.interview.activity;

import static com.nextive.interview.ApplicationManager.fetchService;
import static com.nextive.interview.activity.view.DefaultTextValuesHolder.fetchValueFor;
import static com.nextive.interview.service.WeightInfoService.DECIMAL_FORMAT;

import java.util.Calendar;
import java.util.Date;

import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.Dialog;
import android.app.TimePickerDialog;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.TimePicker;

import com.nextive.interview.R;
import com.nextive.interview.activity.SetupActivity.SettingsMode;
import com.nextive.interview.activity.view.DefaultTextSwitcherFactory;
import com.nextive.interview.dao.WeightInfoDao;
import com.nextive.interview.dto.UserPreferences;
import com.nextive.interview.dto.WeightDateSingleLog;
import com.nextive.interview.service.UserInfoService;
import com.nextive.interview.service.UserInfoService.Metric;
import com.nextive.interview.service.WeightInfoService;
import com.nextive.interview.util.DateUtil;
import com.nextive.interview.util.ValidationUtil;

/**
 * Activity class for the use of edition of a Weight - Date entry.
 * It is in charge of editing as well as creating new entry.
 * @author Cesar Roman
 *
 */
public class EditWeightEntryActivity extends Activity {
	
	/**
	 * Weight entry specific id to be edited.
	 */
	private Long weightEntryId;
	
	/**
	 * Date text switcher display.
	 */
	private TextSwitcher dateDisplay;
	
	/**
	 * Time text switcher display.
	 */
	private TextSwitcher timeDisplay;

	/**
	 * Change date button.
	 */
	private Button pickDateButton;
	
	/**
	 * Date dialog to pick up a date.
	 */
	private DatePickerDialog dateDialog;
	
	/**
	 * Time dialog to pick up time.
	 */
	private TimePickerDialog timePickerDialog;
	
	/**
	 * Confirm changes button.
	 */
	private Button confirmButton;
	
	/**
	 * Cancel button, go back to previous state without saving.
	 */
	private Button cancelButton;
	
	// dialog id's
	private static final int DATE_DIALOG_ID = 0;
	private static final int TIME_DIALOG_ID = 1;
	
	// maximum and minimum weight values
	private static final Double MAX_POUNDS_WEIGHT = 660d;
	private static final Double MAX_KG_WEIGHT = 300d;
	private static final Double MIN_WEIGHT = 0d;
	
	/**
	 * Decrease symbol used to decrease weight.
	 */
	private TextView decreaseWeightDisplay;
	
	/**
	 * Increase symbol used to increase weight.
	 */
	private TextView increaseWeightDisplay;
	
	/**
	 * Text display for the weight.
	 */
	private TextView weightDisplay;
	
	/**
	 * Metric display. It gets the value from corresponding <code>METRIC</code> enum.
	 */
	private TextView metricWeightDisplay;
	
	/**
	 * Flag used by <code>FastCounterThread</code> in order to determine when to stop increasing / decreasing weight. 
	 */
	private volatile boolean fasterCounterActive = false;
	
	/**
	 * Handler used by <code>FastCounterThread</code> in order to update the UI.
	 */
	private Handler handler;
	
	/**
	 * Service to obtain weight related info.
	 */
	private WeightInfoService weightInfoService;

	/**
	 * Service to obtain user preferences related info.
	 */
	private UserInfoService userInfoService;
	
	/**
	 * DTO object to transfer data from/to service layer.
	 */
	private WeightDateSingleLog weightDateLog;
	
	/**
	 * DTO object containing user preferences info.
	 */
	private UserPreferences userPreferences;
	
	/**
	 * Main method for this activity. It is in charge of configuring all layouts of the screen.
	 * @param savedInstanceState
	 */
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		
		super.onCreate(savedInstanceState);
		weightDateLog = (WeightDateSingleLog) getLastNonConfigurationInstance();
		userInfoService = fetchService(UserInfoService.class);
		weightInfoService = fetchService(WeightInfoService.class);
		handler = new Handler();

		setContentView(R.layout.weight_edit);
		
		bindInputs();
		configurePickDateButton();
		configureConfirmButton();
		configureCancelButton();
		configureDateDisplay();
		configureWeightDisplay();
		
	}

	/**
	 * It checks if we are currently editing a weight entry, therefore setting 
	 * corresponding id for future edition. It then populates all info.
	 */
    @Override
	protected void onResume() {
		
		super.onResume();
		
		if (weightEntryId == null) {
			
			// check if we are editing
			Bundle extras = getIntent().getExtras();
			if (extras != null)
				weightEntryId = Long.parseLong(extras.getSerializable(WeightInfoDao.ID_KEY).toString());
			
		}
		
		populateWeightInfo();
		
	}

	/**
	 * It creates the options menu. This is done via inflating menu.
	 */
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.settings_options_menu, menu);
		return true;
		
	}

	/**
	 * It forwards flow to setup activity.
	 */
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		
		switch (item.getItemId()) {
		case R.id.settings_menu:
			forwardToSettingsActivity();
		}
		
		return super.onOptionsItemSelected(item);
	}

	/**
	 * It creates dialog to change the date.
	 */
	@Override
	protected Dialog onCreateDialog(int id) {
		
		switch (id) {
		case DATE_DIALOG_ID:
			return dateDialog;
		case TIME_DIALOG_ID:
			return timePickerDialog;
		}
		
		return null;
		
	}

    /**
     * It saves partial information so to be populated when activity comes to foreground again.
     * @return a WeightDateSingleLog
     */
	@Override
	public Object onRetainNonConfigurationInstance() {

		weightDateLog.setWeight(Double.parseDouble(weightDisplay.getText().toString()));
		return weightDateLog;
		
	}

	/**
	 * It saves current state of the data. Please note that it could be partial as well, meaning 
	 * that whenever we leave this activity we'll be performing a save; unless it is a cancel.
	 * @return true if saved successfully
	 */
	private boolean saveState() {
    	
		if (performValidation()) {

	    	if (weightEntryId == null)
	    		// create a new weight entry
	    		weightEntryId = weightInfoService.createNewWeightEntry(weightDateLog);
	    	else
	    		// update weight entry
	    		weightInfoService.updateWeightInfo(weightDateLog);
	    	
	    	ValidationUtil.acceptValidationForView(R.string.weight_info_saved);
	    	return true;
	    	
		}
		
		return false;
    	
    }
	
	/**
	 * It performs validation of the date.
	 * @return true if success
	 */
	private boolean performValidation() {
		
		if (!ValidationUtil.isValidDate(weightDateLog.getCalendar())) {

			ValidationUtil.rejectValidationForView(findViewById(
					R.id.time_widget_id), R.string.time_error);
			return false;
			
		}
		
		return true;
		
	}
	
	/**
	 * It populates all input fields on screen with weight info. 
	 */
	private void populateWeightInfo() {
		
		userPreferences = userInfoService.fetchUserInfo();
		if (weightDateLog == null) {

			if (weightEntryId != null) {
				
				// updating an existing entry
				weightDateLog = weightInfoService.fetchWeightInfo(weightEntryId);
				
			} else {
				
				// when creating new entry populate last registered weight
				weightDateLog = new WeightDateSingleLog();
				weightDateLog.setCalendar(Calendar.getInstance());
				Double weight = weightInfoService.fetchLastRegisteredWeight();
				weightDateLog.setWeight(weight == null ? Double.parseDouble(
						fetchValueFor(R.string.default_weight_number).toString()) : weight);
				
			}

		}
		
		updateDateLayout();
		updateTimeLayout();
		updateWeightDisplay();
			
	}

	/**
	 * It binds all inputs to member fields so they can be referenced later on.
	 */
	private void bindInputs() {
		
		dateDisplay = (TextSwitcher) findViewById(R.id.dateDisplay);
		timeDisplay = (TextSwitcher) findViewById(R.id.timeDisplay);
		confirmButton = (Button) findViewById(R.id.confirm_edit);
		cancelButton = (Button) findViewById(R.id.cancel_edit);
		pickDateButton = (Button) findViewById(R.id.pickDate);
		weightDisplay = (TextView) findViewById(R.id.weight_display_id);
		increaseWeightDisplay = (TextView) findViewById(R.id.increase_weight_id);
		decreaseWeightDisplay = (TextView) findViewById(R.id.decrease_weight_id);
		metricWeightDisplay = (TextView) findViewById(R.id.metric_display_id);
		
	}

	/**
	 * It configures weight widget.
	 */
	private void configureWeightDisplay() {
		
		// set on click listeners for both increase/decrease
		increaseWeightDisplay.setOnClickListener(new WeightUIUpdater(true));
		decreaseWeightDisplay.setOnClickListener(new WeightUIUpdater(false));
		
		// set on long click listeners for both increase/decrease
		increaseWeightDisplay.setOnLongClickListener(new OnLongClickListener() {
			
			@Override
			public boolean onLongClick(View v) {
				
				new FastCounterThread(true).start();
				return true;
				
			}
		});
		decreaseWeightDisplay.setOnLongClickListener(new OnLongClickListener() {
			
			@Override
			public boolean onLongClick(View v) {
				
				new FastCounterThread(false).start();
				return true;
				
			}
		});
		
		// set on touch listeners for both increase/decrease
		increaseWeightDisplay.setOnTouchListener(new FastCounterOnTouchListener());
		decreaseWeightDisplay.setOnTouchListener(new FastCounterOnTouchListener());
		
	}

    /**
     * It configures date text switcher display. It sets the factory and animations.
     */
	private void configureDateDisplay() {
		
    	dateDisplay.setFactory(new DefaultTextSwitcherFactory(R.style.InsideRowSmallTextStyle));
    	timeDisplay.setFactory(new DefaultTextSwitcherFactory(R.style.InsideRowSmallTextStyle));
    	Animation in = AnimationUtils.loadAnimation(this,
                android.R.anim.slide_in_left);
        Animation out = AnimationUtils.loadAnimation(this,
                android.R.anim.slide_out_right);
        dateDisplay.setInAnimation(in);
        dateDisplay.setOutAnimation(out);
        timeDisplay.setInAnimation(in);
        timeDisplay.setOutAnimation(out);
        
        final Calendar calendar = Calendar.getInstance();
        dateDialog = new DatePickerDialog(this, new OnDateSetListener() {
			
			@Override
			public void onDateSet(DatePicker view, int yearIn, int monthOfYear,
					int dayOfMonth) {

				weightDateLog.getCalendar().set(Calendar.YEAR, yearIn);
				weightDateLog.getCalendar().set(Calendar.MONTH, monthOfYear);
				weightDateLog.getCalendar().set(Calendar.DAY_OF_MONTH, dayOfMonth);;
				showDialog(TIME_DIALOG_ID);
				
			}
		}, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
		timePickerDialog = new TimePickerDialog(this, new OnTimeSetListener() {
			
			@Override
			public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
				
				weightDateLog.getCalendar().set(Calendar.HOUR_OF_DAY, hourOfDay);
				weightDateLog.getCalendar().set(Calendar.MINUTE, minute);
				updateDateLayout();
				updateTimeLayout();
				
			}
			
		}, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), false);
	}
	
	/**
	 * It configures confirm button. Whenever this button is clicked,  data is saved and activity 
	 * is considered as finished.
	 */
	private void configureConfirmButton() {

		confirmButton.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
			
				if (performValidation()) {

					if (saveState()) {

						setResult(RESULT_OK);
						finish();

					}

				}
				
			}
		});

	}

	/**
	 * It configures confirm button. Whenever this button is clicked, activity 
	 * is considered as finished without any savings.
	 */
	private void configureCancelButton() {

		cancelButton.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
			
				// set cancel flag to true
				setResult(RESULT_OK);
				finish();
				
			}
		});

	}

	/**
	 * It configures pick date button. Whenever this button is clicked, a dialog will be shown.
	 */
	private void configurePickDateButton() {

		pickDateButton.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
			
				showDialog(DATE_DIALOG_ID);
				
			}
		});

	}

    /**
	 * It updates date display text and dialog. It takes inputs from year, month and day to make 
	 * a single text and update its dialog.
	 */
    private void updateDateLayout() {

    	Calendar calendar = weightDateLog.getCalendar();
	    dateDisplay.setText(DateUtil.generateVisualDate(calendar));
		dateDialog.updateDate(
				calendar.get(Calendar.YEAR), 
				calendar.get(Calendar.MONTH), 
				calendar.get(Calendar.DAY_OF_MONTH));

	}
    
    /**
	 * It updates time display text and dialog. It takes inputs from hour, minutes to make 
	 * a single text and update its dialog.
	 */
    private void updateTimeLayout() {
    	
    	Calendar calendar = weightDateLog.getCalendar();
    	timeDisplay.setText(DateUtil.generateVisualTime(calendar));
    	
    	timePickerDialog.updateTime(
    			calendar.get(Calendar.HOUR_OF_DAY), 
    			calendar.get(Calendar.MINUTE));
    	
    }
	
    /**
     * It updates weight display, this means updating weight text as well as metric text.
     */
    private void updateWeightDisplay() {
    	
    	if (weightDateLog.getWeight() != null)
    		weightDisplay.setText(DECIMAL_FORMAT.format(weightDateLog.getWeight()));
    	
    	if (userPreferences.getMetricOption() != null) {

    		metricWeightDisplay.setText(
    				Metric.KG.equals(userPreferences.getMetricOption()) ? fetchValueFor(R.string.kg_metric) :
    				fetchValueFor(R.string.lb_metric));
    		
    	}
    	
    }
	
	/**
	 * It forwards flow to settings activity. It puts into the intent 
	 * <code>CHANGE_SETTIGNS</code> flag so to be able to update user settings.
	 */
	private void forwardToSettingsActivity() {
		
		Intent intent = new Intent(this, SetupActivity.class);
		intent.putExtra(SettingsMode.CHANGE_SETTINGS.name(), true);
		
		startActivity(intent);
		
	}
	
	/**
	 * This class is in charge of updating weight by the minimum unit.
	 * It is being used when doing a click and long click.
	 * @author Cesar Roman
	 *
	 */
	private class WeightUIUpdater implements Runnable, OnClickListener {

		/**
		 * Flag indicating whether it is an increase or decrease.
		 */
		private boolean increase;
		
		/**
		 * Minimum unit for increasing/decreasing weight.
		 */
		private static final double MINIMUM_UNIT = 0.1d;
		
		/**
		 * Constructor receiving increase/decrease flag.
		 * @param increase true if it should be an increase
		 */
		public WeightUIUpdater(boolean increase) {
			
			this.increase = increase;
			
		}
		
		/**
		 * Implementation of Runnable run method.
		 */
		@Override
		public void run() {

			doUpdate();
			
		}

		/**
		 * Implementation of OnClickListener interface.
		 * @param v
		 */
		@Override
		public void onClick(View v) {
			
			doUpdate();
			
		}
		
		/**
		 * It performs the update or the weight. It checks for maximum and minimum weights.
		 */
		private void doUpdate() {

			Double weight = Double.parseDouble(weightDisplay.getText().toString());
			if (increase) {
				Double maxWeight;
				if (Metric.KG.equals(userPreferences.getMetricOption()))
					maxWeight = MAX_KG_WEIGHT;
				else
					maxWeight = MAX_POUNDS_WEIGHT;
				if (weight < maxWeight)
					weightDateLog.setWeight(weight + MINIMUM_UNIT);
			} else if (weight > MIN_WEIGHT)
				weightDateLog.setWeight(weight - MINIMUM_UNIT);
			
			updateWeightDisplay();
			
		}

	}
	
	/**
	 * This class is in charge of executing a fast update of the weight.
	 * Depending on how long the on long click is been active, it then 
	 * decreases the sleep time so we finish having a faster increaser/decreaser. 
	 * @author Cesar Roman
	 *
	 */
	private class FastCounterThread extends Thread {

		/**
		 * Default time to sleep.
		 */
		private static final long DEFAULT_SLEEP_TIME = 65;
		
		/**
		 * Portion of sleep time to be decreased. 
		 */
		private static final long DECREASE_PORTION_TIME = 30;
		
		/**
		 * Minimum portion of time to take into account when 
		 * doing a faster increase/decrease.
		 */
		private static final long INCREASE_TIME = 2500;
		
		/**
		 * Flag indicating whether it is an increase or decrease.
		 */
		private boolean increase;
		
		/**
		 * Constructor receiving a flag indicating this is an increase / decrease.
		 * @param shouldIncrease flag indicating this is icnrease or decrease
		 */
		FastCounterThread(boolean shouldIncrease) {
			
			this.increase = shouldIncrease;
			
		}
		
		/**
		 * It executes the fast increasing/decreasing.
		 */
		@Override
		public void run() {
			
			fasterCounterActive = true;
			long sleepTime = DEFAULT_SLEEP_TIME;
			long startTime = new Date().getTime();
			while (fasterCounterActive) {
				
				handler.post(new WeightUIUpdater(increase));
				
				try {
					Thread.sleep(sleepTime);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				if (sleepTime > DECREASE_PORTION_TIME && (new Date().getTime() - startTime) >= INCREASE_TIME) {
					
					// increase/decrease faster
					sleepTime -= DECREASE_PORTION_TIME;
					startTime = new Date().getTime();
					
				}

			}
			
		}
		
	}
	
	/**
	 * This class is in charge of listening to touch events so to determine 
	 * when to stop increasing/decreasing. 
	 * @author Cesar Roman
	 *
	 */
	private class FastCounterOnTouchListener implements OnTouchListener {
		
		/**
		 * Implementation of onTouch method. It listens to <code>ACTION_UP</code> 
		 * event so to release increasing/decreasing.
		 * @param v view involved in the event
		 * @param event specific event
		 * @return always false
		 */
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			
			if (MotionEvent.ACTION_UP == event.getAction()) {
				
				fasterCounterActive = false;
				
			}
				
			return false;
			
		}

	}
	
}
